package org.samoji.sji.tokenizer;

import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;

public class Tokenizer {
	private Token prevToken;
	private Token token;

	private boolean prevMode = false;

	private int currentCh;
	private Reader stream;

	private final int EOS = -1;

	public Tokenizer(Reader in) {
		this.stream = in;
	}

	/**
	 * Return the next token
	 * 
	 * @return the current token
	 * @throws IOException
	 */
	public Token next() throws IOException {
		skipWhitesAndComments();
		
		token = nextNum();
		if (token != null)
			return token;

		token = nextID();
		if (token != null)
			return token;

		token = nextOP();
		if (token != null)
			return token;

		token = nextAssign();
		if (token != null)
			return token;
		
		token = nextEOL();
		if (token != null)
			return token;
		
		return new ErrToken("Error", "Unknown token");
	}

	private Token nextEOL() throws IOException {
		if (isEOL()) {
			return new Token("EOL", "End of line");
		} else {
			reset();
			return null;
		}
	}

	private Token nextAssign() throws IOException {
		markAndRead(1);
		
		if (isAssign()) {
			return new Token("=", "Assignment operator");
		} else {
			reset();
			return null;
		}
	}

	private boolean isAssign() {
		if ((char)currentCh == '=')
			return true;
		return false;
	}

	/**
	 * Check if the current character is an operation sign
	 **/
	private Token nextOP() throws IOException {
		markAndRead(1);
		if (!isEOS() && (isSign() || isOperator())) {
			return new OPToken("OP", "Is an operation", (char)currentCh);
		} else {
			reset();
			return null;
		}
		
	}

	private boolean isOperator() {
		if (currentCh == '*' || currentCh == '/')
			return true;
		return false;
	}
	
	private boolean isEOL() {
		if ((char)currentCh == ';')
			return true;
		return false;
	}
	
	/**
	 * Check if the current character is valid to be a
	 *	start condition for a Num token
	 * 
	 * @return
	 * @throws IOException
	 */
	private Token nextNum() throws IOException {
		markAndRead(1);

		if (!isEOS() && (isDigit() || isDot())) {
			return numBuilder();
		} else if (isEOS()) {
			return new EOSToken("EOS", "End of stream");
		} else {
			reset();
			return null;
		}
	}

	/**
	 * Create a NUM token
	 * 
	 * @return a NumToken
	 * @throws IOException
	 */
	private Token numBuilder() throws IOException {
		// TODO: spostare i reset() nei metodi

		StringBuilder sb = new StringBuilder();

		sb.append(readSequenceNumber());

		if (isDot()) {
			sb.append(readSequenceNumber());
		}
		reset();

		sb.append(nextPower());

		reset();

		return new NumToken("NUM", "Num token", new BigDecimal(sb.toString()));
	}

	private StringBuilder nextPower() throws IOException {
		markAndRead(3);
		if (isPower() && !isEOS()) {
			return powerBuilder();
		} else {
			reset();
			return new StringBuilder();
		}

	}

	private StringBuilder powerBuilder() throws IOException {
		StringBuilder power = new StringBuilder();
		power.append((char) currentCh);

		read();
		if (isSign()) {
			power.append((char) currentCh);
			read();
		}

		if (isDigit()) {
			power.append(readSequenceNumber());

			return power;
		} else {
			reset();

			return new StringBuilder();
		}
	}

	private StringBuilder readSequenceNumber() throws IOException {
		StringBuilder temp = new StringBuilder();

		do {
			temp.append((char) currentCh);
			markAndRead(1);
		} while (isDigit());
		// reset();

		return temp;
	}

	/**
	 * Check if the current character is a dot
	 * 
	 * @return true if is a dot else false
	 */
	private boolean isDot() {
		if (currentCh == '.')
			return true;
		return false;
	}

	/**
	 * Read without mark
	 * 
	 * @throws IOException
	 */
	private void read() throws IOException {
		currentCh = stream.read();
	}

	/**
	 * Check the current character
	 * 
	 * @return true if the sign is "+" / "-"
	 */
	private boolean isSign() {
		if (currentCh == '+' || currentCh == '-')
			return true;
		return false;
	}

	/**
	 * Check if the current character is a power start
	 * 
	 * @return true: if the currentCH is E or e <br />
	 *         false: otherwise
	 */
	private boolean isPower() {
		if (currentCh == '\u0065' || currentCh == '\u0045')
			return true;
		return false;
	}

	/**
	 * Check if the current character is valid to be a start condition for an ID
	 * token
	 * 
	 * @return IDToken: if the current character is valid<br />
	 *         EOSToken: if the current character is -1<br />
	 *         null: otherwise
	 * @throws IOException
	 */
	private Token nextID() throws IOException {
		markAndRead(1);
		if (!isEOS() && isLetter()) {
			return IDBuilder();
		} else if (isEOS()) {
			return new EOSToken("EOS", "End of stream");
		} else {
			reset();
			return null;
		}
	}

	/**
	 * Create an ID token
	 * 
	 * @return an IDToken
	 * @throws IOException
	 */
	private Token IDBuilder() throws IOException {
		StringBuilder sb = new StringBuilder();

		do {
			sb.append((char) currentCh);
			markAndRead(1);
		} while (isDigit() || isLetter());
		reset();

		return new IDToken("ID", "ID token", sb.toString());
	}

	/**
	 * Mark the current position in the stream and read the next character
	 * 
	 * @param k
	 *            : memory
	 * @throws IOException
	 */
	private void markAndRead(int k) throws IOException {
		stream.mark(k);
		currentCh = stream.read();
	}

	/**
	 * Reset the reader
	 * 
	 * @throws IOException
	 */
	private void reset() throws IOException {
		stream.reset();
	}

	/**
	 * 
	 * @return true: if the current character is -1
	 */
	private boolean isEOS() {
		return (currentCh == EOS);
	}

	/**
	 * Check if the current character is a digit
	 * 
	 * @return true: if the current character is a digit<br />
	 *         false: if is not
	 */
	private boolean isDigit() {
		return (Character.isDigit(currentCh));
	}

	/**
	 * Check if the current character is a letter
	 * 
	 * @return true: if the current character is a letter<br />
	 *         false: if is not
	 */
	private boolean isLetter() {
		return (Character.isLetter(currentCh));
	}

	private boolean isWhiteSpace() {
		return Character.isWhitespace(currentCh);
	}

	/**
	 * Positionate the stream at the end of whites
	 * end comments
	 * 
	 * @throws IOException
	 */
	private void skipWhitesAndComments() throws IOException {
		do {
			markAndRead(1);
			
			if (isCommentStart())
				skipComment();
			else
				reset();
		} while (isWhiteSpace() && !isEOS());
		reset();
		
		/*if (isCommentStart()) {
			do {
				markAndRead(1);
			} while (!isCommentEnd() && !isEOS());
		}
		reset();*/
	}
	
	private void skipComment() throws IOException {
		if (isCommentStart()) {
			do {
				markAndRead(1);
			} while (!isCommentEnd() && !isEOS());
		}
		reset();
	}
	
	/*private boolean nextComment() throws IOException {
		markAndRead(2);
		
		if (isComment()) {
			return true;
		} else {
			reset();
			return false;
		}
	}*/
	
	private boolean isCommentStart() throws IOException {
		stream.mark(2);
		
		if ((char)currentCh =='/') {
			read();
			if ((char)currentCh == '*') {
				stream.mark(1);
				return true;
			} else {
				reset();
				return false;
			}
		} else {
			reset();
			return false;
		}
	}
	
	private boolean isCommentEnd() throws IOException {
		stream.mark(2);
		
		if ((char)currentCh =='*') {
			read();
			if ((char)currentCh == '/') {
				stream.mark(1);
				return true;
			} else {
				reset();
				return false;
			}
		} else {
			reset();
			return false;
		}
	}
	
	private boolean isParenthesis() {
		return true;
	}

	/**
	 * Inform if there are more token
	 * 
	 * @return true: if there are still token<br />
	 *         false: otherwise
	 * @throws IOException
	 */
	public boolean hasNext() throws IOException {
		if (token != null && token.getType().equals("EOS")) {
			return false;
		} else {
			return true;
		}
	}

}
